﻿using System.Runtime.InteropServices;
using Avalonia;
using Avalonia.Media.Imaging;
using CoreFoundation;
using Everywhere.Interop;
using ImageIO;
using ObjCRuntime;

namespace Everywhere.Mac.Interop;

/// <summary>
/// Provides interop methods for macOS Accessibility API (AXUIElement).
/// </summary>
public partial class AXUIElement : NSObject, IVisualElement
{
    public string Id => $"{ProcessId}.{NativeWindowHandle}.{CoreFoundationInterop.CFHash(Handle)}";

    public IVisualElement? Parent => field ??= GetAttributeAsElement(AXAttributeConstants.Parent);

    public VisualElementSiblingAccessor SiblingAccessor => new SiblingAccessorImpl(this);

    public IEnumerable<IVisualElement> Children
    {
        get
        {
            var children = GetAttribute<NSArray>(AXAttributeConstants.Children);
            if (children is null) yield break;

            for (nuint i = 0; i < children.Count; i++)
            {
                if (children.GetItem<AXUIElement>(i) is { } child)
                {
                    yield return child;
                }
            }
        }
    }

    public AXRoleAttribute Role { get; }

    public VisualElementType Type
    {
        get
        {
            // This requires a mapping from AXRole/AXSubrole to VisualElementType
            return Role switch
            {
                AXRoleAttribute.AXStaticText => VisualElementType.Label,
                AXRoleAttribute.AXTextField or AXRoleAttribute.AXTextArea => VisualElementType.TextEdit,
                AXRoleAttribute.AXBrowser => VisualElementType.Document,
                AXRoleAttribute.AXButton or AXRoleAttribute.AXMenuButton or AXRoleAttribute.AXPopUpButton => VisualElementType.Button,
                AXRoleAttribute.AXLink => VisualElementType.Hyperlink,
                AXRoleAttribute.AXImage => VisualElementType.Image,
                AXRoleAttribute.AXCheckBox => VisualElementType.CheckBox,
                AXRoleAttribute.AXRadioButton => VisualElementType.RadioButton,
                AXRoleAttribute.AXComboBox => VisualElementType.ComboBox,
                AXRoleAttribute.AXList => VisualElementType.ListView,
                AXRoleAttribute.AXOutline => VisualElementType.TreeView,
                AXRoleAttribute.AXTabGroup => VisualElementType.TabControl,
                AXRoleAttribute.AXTable or AXRoleAttribute.AXSheet => VisualElementType.Table,
                AXRoleAttribute.AXRow => VisualElementType.TableRow,
                AXRoleAttribute.AXMenuBar or AXRoleAttribute.AXMenu or AXRoleAttribute.AXToolbar => VisualElementType.Menu,
                AXRoleAttribute.AXMenuBarItem or AXRoleAttribute.AXMenuItem => VisualElementType.MenuItem,
                AXRoleAttribute.AXSlider => VisualElementType.Slider,
                AXRoleAttribute.AXScrollBar => VisualElementType.ScrollBar,
                AXRoleAttribute.AXBusyIndicator or
                    AXRoleAttribute.AXProgressIndicator or
                    AXRoleAttribute.AXValueIndicator => VisualElementType.ProgressBar,
                AXRoleAttribute.AXColumn or
                    AXRoleAttribute.AXDrawer or
                    AXRoleAttribute.AXGrid or
                    AXRoleAttribute.AXGroup or
                    AXRoleAttribute.AXGrowArea or
                    AXRoleAttribute.AXPage or
                    AXRoleAttribute.AXScrollArea or
                    AXRoleAttribute.AXSplitGroup or
                    AXRoleAttribute.AXSplitter or
                    AXRoleAttribute.AXWebArea => VisualElementType.Panel,
                AXRoleAttribute.AXApplication or AXRoleAttribute.AXWindow => VisualElementType.TopLevel,
                // TODO: ... add more mappings
                _ => VisualElementType.Unknown
            };
        }
    }

    public VisualElementStates States
    {
        get
        {
            var states = VisualElementStates.None;
            if (GetAttribute<NSNumber>(AXAttributeConstants.Enabled)?.BoolValue == false) states |= VisualElementStates.Disabled;
            if (GetAttribute<NSNumber>(AXAttributeConstants.Focused)?.BoolValue == true) states |= VisualElementStates.Focused;
            // TODO: add more state checks?
            return states;
        }
    }

    public string? Name => GetAttribute<NSString>(AXAttributeConstants.Title);

    public PixelRect BoundingRectangle
    {
        get
        {
            try
            {
                var posVal = GetAttribute<AXValue>(AXAttributeConstants.Position);
                var sizeVal = GetAttribute<AXValue>(AXAttributeConstants.Size);

                if (posVal is null || sizeVal is null) return default;

                var pos = posVal.Point;
                var size = sizeVal.Size;
                return new PixelRect((int)pos.X, (int)pos.Y, (int)size.Width, (int)size.Height);
            }
            catch
            {
                return default;
            }
        }
    }

    public int ProcessId => GetPid(Handle, out var pid) == AXError.Success ? pid : 0;

    public nint NativeWindowHandle => GetWindow(Handle, out var windowId) == AXError.Success ? (nint)windowId : 0;

    static AXUIElement()
    {
        // default timeout for AX calls is 6s, which is too long for our use case.
        AXUIElementSetMessagingTimeout(SystemWide.Handle.Handle, 1f);
    }

    private AXUIElement(NativeHandle handle) : base(handle, true)
    {
        var axRole = GetAttribute<NSString>(AXAttributeConstants.Role);
        Role = Enum.TryParse<AXRoleAttribute>(axRole, true, out var role) ? role : AXRoleAttribute.AXUnknown;
    }

    public string? GetText(int maxLength = -1)
    {
        var text = GetAttribute<NSObject>(AXAttributeConstants.Value)?.ToString();
        if (string.IsNullOrEmpty(text)) return null;

        if (Role == AXRoleAttribute.AXCheckBox) return text == "0" ? "false" : "true"; // don't apply trim

        return maxLength > 0 && text.Length > maxLength ? text[..maxLength] : text;
    }

    // TODO: Is press enough?
    public void Invoke() => PerformAction(AXAttributeConstants.Press);

    public void SetText(string text)
    {
        using var nsText = new NSString(text);
        SetAttributeValue(Handle, AXAttributeConstants.Value.Handle, nsText.Handle);
    }

    public void SendShortcut(KeyboardShortcut shortcut)
    {
        // This is complex on macOS. It usually involves using CoreGraphics CGEventCreateKeyboardEvent
        // to create and post keyboard events to the process that owns the element.
        throw new NotImplementedException();
    }

    public string? GetSelectionText() => GetAttribute<NSString>(AXAttributeConstants.SelectedText);

    public Task<Bitmap> CaptureAsync(CancellationToken cancellationToken)
    {
        var bounds = BoundingRectangle;
        var rect = new CGRect(bounds.X, bounds.Y, bounds.Width, bounds.Height);
        var windowId = NativeWindowHandle;

#pragma warning disable CA1422 // Type or member is obsolete
        // we use CGSHWCaptureWindowList because it can screenshot minimized windows, which CGWindowListCreateImage can't
        using var cgImage = SkyLightInterop.HardwareCaptureWindowList(
            [(uint)windowId],
            SkyLightInterop.CGSWindowCaptureOptions.IgnoreGlobalCLipShape |
            SkyLightInterop.CGSWindowCaptureOptions.FullSize);
#pragma warning restore CA1422

        if (cgImage is null)
        {
            return Task.FromException<Bitmap>(new InvalidOperationException("Failed to capture screen image."));
        }

        using var data = new NSMutableData();
        using var dest = CGImageDestination.Create(data, "public.png", 1);

        if (dest is null)
        {
            return Task.FromException<Bitmap>(new InvalidOperationException("Failed to create image destination."));
        }

        if (!rect.IsEmpty)
        {
            var screen = NSScreen.Screens.FirstOrDefault(s => s.Frame.IntersectsWith(rect));
            var scale = screen?.BackingScaleFactor ?? 1.0;
            using var croppedImage = cgImage.WithImageInRect(new CGRect(
                rect.X * scale,
                rect.Y * scale,
                rect.Width * scale,
                rect.Height * scale));

            if (croppedImage is null)
            {
                return Task.FromException<Bitmap>(new InvalidOperationException("Failed to crop image."));
            }

            dest.AddImage(croppedImage);
            dest.Close();

            // after this, we can safely dispose data
            return Task.FromResult(new Bitmap(data.AsStream()));
        }

        dest.AddImage(cgImage);
        dest.Close();

        // after this, we can safely dispose data
        return Task.FromResult(new Bitmap(data.AsStream()));
    }

    public override bool Equals(object? obj)
    {
        return obj is AXUIElement element && CFType.Equal(Handle, element.Handle);
    }

    public override int GetHashCode()
    {
        return CoreFoundationInterop.CFHash(Handle).GetHashCode();
    }

    #region Helpers

    private IEnumerable<string> GetAttributeNames()
    {
        var error = CopyAttributeNames(Handle, out var namesHandle);
        if (error != AXError.Success || namesHandle == 0) return [];

        var namesArray = CFArray.StringArrayFromHandle(namesHandle);
        return namesArray?.OfType<string>() ?? [];
    }

    private T? GetAttribute<T>(NSString attributeName) where T : NSObject
    {
        var error = CopyAttributeValue(Handle, attributeName.Handle, out var value);
        return error == AXError.Success ? Runtime.GetNSObject<T>(value) : null;
    }

    private AXUIElement? GetAttributeAsElement(NSString attributeName)
    {
        var error = CopyAttributeValue(Handle, attributeName.Handle, out var value);
        if (error == AXError.Success && value != 0)
        {
            return new AXUIElement(value);
        }

        return null;
    }

    private void PerformAction(NSString actionName)
    {
        var error = PerformAction(Handle, actionName.Handle);
        if (error != AXError.Success)
        {
            throw new InvalidOperationException($"Failed to perform action {actionName}. Error: {error}");
        }
    }

    private const string AppServices = "/System/Library/Frameworks/ApplicationServices.framework/ApplicationServices";

    public static AXUIElement SystemWide { get; } = new(CreateSystemWide());

    public AXUIElement? ElementAtPosition(float x, float y)
    {
        var error = CopyElementAtPosition(Handle, x, y, out var element);
        return error == AXError.Success && element != 0 ? new AXUIElement(element) : null;
    }

    public AXUIElement? ElementByAttributeValue(NSString attributeName)
    {
        var error = CopyAttributeValue(Handle, attributeName.Handle, out var value);
        return error == AXError.Success && value != 0 ? new AXUIElement(value) : null;
    }

    public static AXUIElement? ElementFromPid(int pid)
    {
        var handle = CreateApplication(pid);
        return handle != 0 ? new AXUIElement(handle) : null;
    }

    [LibraryImport(AppServices, EntryPoint = "AXUIElementCreateSystemWide")]
    private static partial nint CreateSystemWide();

    [LibraryImport(AppServices, EntryPoint = "AXUIElementSetMessagingTimeout")]
    private static partial AXError AXUIElementSetMessagingTimeout(nint element, float timeoutInSeconds);

    [LibraryImport(AppServices, EntryPoint = "AXUIElementCopyElementAtPosition")]
    private static partial AXError CopyElementAtPosition(nint application, float x, float y, out nint element);

    [LibraryImport(AppServices, EntryPoint = "AXUIElementCopyAttributeNames")]
    private static partial AXError CopyAttributeNames(nint element, out nint names);

    [LibraryImport(AppServices, EntryPoint = "AXUIElementCopyAttributeValue")]
    private static partial AXError CopyAttributeValue(nint element, nint attribute, out nint value);

    [LibraryImport(AppServices, EntryPoint = "AXUIElementPerformAction")]
    private static partial AXError PerformAction(nint element, nint action);

    [LibraryImport(AppServices, EntryPoint = "AXUIElementSetAttributeValue")]
    private static partial AXError SetAttributeValue(nint element, nint attribute, nint value);

    /// <summary>
    /// Private API from https://github.com/lwouis/alt-tab-macos/blob/9761bb91e97646f1c30b43842c4694615e9ad39b/src/api-wrappers/private-apis/ApplicationServices.HIServices.framework.swift#L5
    /// </summary>
    [LibraryImport(AppServices, EntryPoint = "_AXUIElementGetWindow")]
    private static partial AXError GetWindow(nint element, out uint cgWindowId);

    [LibraryImport(AppServices, EntryPoint = "AXUIElementGetPid")]
    private static partial AXError GetPid(nint element, out int pid);

    [LibraryImport(AppServices, EntryPoint = "AXUIElementCreateApplication")]
    private static partial nint CreateApplication(int pid);

    #endregion

    private class SiblingAccessorImpl(AXUIElement origin) : VisualElementSiblingAccessor
    {
        private NSArray? _siblings;
        private nint _index;

        protected override void EnsureResources()
        {
            if (_siblings is not null) return;
            if (origin.Parent is not AXUIElement parent) return;

            _siblings = parent.GetAttribute<NSArray>(AXAttributeConstants.Children);
            _index = _siblings is not null ? (nint)_siblings.IndexOf(origin) : nint.MaxValue;
        }

        protected override void ReleaseResources()
        {
            if (_siblings is null) return;

            _siblings.Dispose();
            _siblings = null;
        }

        protected override IEnumerator<IVisualElement> CreateForwardEnumerator()
        {
            if (_siblings is not { } siblings || _index == nint.MaxValue) yield break;

            var count = (nint)siblings.Count;
            for (var i = _index + 1; i < count; i++)
            {
                if (siblings.GetItem<AXUIElement>((nuint)i) is { } sibling)
                {
                    yield return sibling;
                }
            }
        }

        protected override IEnumerator<IVisualElement> CreateBackwardEnumerator()
        {
            if (_siblings is not { } siblings || _index == nint.MaxValue) yield break;

            for (var i = _index - 1; i >= 0; i--)
            {
                if (siblings.GetItem<AXUIElement>((nuint)i) is { } sibling)
                {
                    yield return sibling;
                }
            }
        }
    }
}